* Moved htmlEscape from mediawiki.util.js to mediawiki.js so that it can be used...
authorTim Starling <tstarling@users.mediawiki.org>
Sun, 7 Nov 2010 23:46:57 +0000 (23:46 +0000)
committerTim Starling <tstarling@users.mediawiki.org>
Sun, 7 Nov 2010 23:46:57 +0000 (23:46 +0000)
* Added mediaWiki.html.element(), which provides a safe HTML construction function similar to Xml::element().
* Used element() in various places in mediawiki.js. Fixes escaping issue noted on CR r75170.
* Profiled the new mediaWiki.html.escape() at 1.4 MB/s for special characters and 154 MB/s for non-special characters on my humble laptop. Hopefully that's fast enough to convince Trevor that escaping is unlikely to be a significant component of page render time.
* Profiled mediaWiki.html.element() generating style elements with Cdata at ~17us per iteration. For comparison, $('body').append('<div/>') takes 200us.

resources/mediawiki.util/mediawiki.util.js
resources/mediawiki.util/mediawiki.util.test.js
resources/mediawiki/mediawiki.js

index 397828c..c09fc70 100644 (file)
                        return null;
                },
 
-               /**
-                * Convert special characters to their HTML entities
-                *
-                * @param str Text to escape
-                */
-               'htmlEscape': function( str ) {
-                       return str.replace( /['"<>&]/g, this.htmlEscape_callback );
-               },
-
-               'htmlEscape_callback': function( str ) {
-                       switch ( str ) {
-                               case "'":
-                                       return '&#039;';
-                               case '"':
-                                       return '&quot;';
-                               case '<':
-                                       return '&lt;';
-                               case '>':
-                                       return '&gt;';
-                               case '&':
-                                       return '&amp;';
-                       }
-               },
-
                // Access key prefix.
                // Will be re-defined based on browser/operating system detection in 
                // mw.util.init().
index f60ffc3..389dc2d 100644 (file)
@@ -28,7 +28,7 @@
                                contain = result;
                        }
                        this.addedTests.push([code, result, contain]);
-                       this.$table.append('<tr><td>' + mw.util.htmlEscape(code) + '</td><td>' + mw.util.htmlEscape(result) + '<td></td></td><td>?</td></tr>');
+                       this.$table.append('<tr><td>' + mw.html.escape(code) + '</td><td>' + mw.html.escape(result) + '<td></td></td><td>?</td></tr>');
                },
 
                /* Initialisation */
                                                        'function (string)');
                                                mw.test.addTest('mw.util.getParamValue( \'action\' )',
                                                        'mwutiltest (string)');
-                                               mw.test.addTest('typeof mw.util.htmlEscape',
-                                                       'function (string)');
-                                               mw.test.addTest('mw.util.htmlEscape( \'<a href="http://mw.org/?a=b&c=d">link</a>\' )',
-                                                       '&lt;a href=&quot;http://mw.org/?a=b&amp;c=d&quot;&gt;link&lt;/a&gt; (string)');
                                                mw.test.addTest('mw.util.tooltipAccessKeyRegexp.constructor.name',
                                                        'RegExp (string)');
                                                mw.test.addTest('typeof mw.util.updateTooltipAccessKeys',
index d3f8367..625cab1 100644 (file)
@@ -468,16 +468,18 @@ window.mediaWiki = new ( function( $ ) {
                        // Add style sheet to document
                        if ( typeof registry[module].style === 'string' && registry[module].style.length ) {
                                $( 'head' )
-                                       .append( '<style type="text/css">' + registry[module].style + '</style>' );
+                                       .append( mediaWiki.html.element( 'style',
+                                               { type: "text/css" },
+                                               new mediaWiki.html.Cdata( registry[module].style )
+                                       ) );
                        } else if ( typeof registry[module].style === 'object' 
                                && !( registry[module].style instanceof Array ) ) 
                        {
                                for ( var media in registry[module].style ) {
-                                       $( 'head' ).append(
-                                               '<style type="text/css" media="' + media + '">' +
-                                               registry[module].style[media] +
-                                               '</style>'
-                                       );
+                                       $( 'head' ).append( mediaWiki.html.element( 'style', 
+                                               { type: 'text/css', media: media },
+                                               new mediaWiki.html.Cdata( registry[module].style[media] )
+                                       ) );
                                }
                        }
                        // Add localizations to message system
@@ -652,7 +654,8 @@ window.mediaWiki = new ( function( $ ) {
                                                requests[r] = sortQuery( requests[r] );
                                                // Build out the HTML
                                                var src = mediaWiki.config.get( 'wgLoadScript' ) + '?' + $.param( requests[r] );
-                                               html += '<script type="text/javascript" src="' + src + '"></script>';
+                                               html += mediaWiki.html.element( 'script', 
+                                                       { type: 'text/javascript', src: src }, '' );
                                        }
                                        return html;
                                }
@@ -711,7 +714,7 @@ window.mediaWiki = new ( function( $ ) {
                 * calls to this function.
                 */
                this.implement = function( module, script, style, localization ) {
-                       // Automaically register module
+                       // Automatically register module
                        if ( typeof registry[module] === 'undefined' ) {
                                mediaWiki.loader.register( module );
                        }
@@ -825,7 +828,8 @@ window.mediaWiki = new ( function( $ ) {
                                                        .attr( 'href', modules ) );
                                                return true;
                                        } else if ( type === 'text/javascript' || typeof type === 'undefined' ) {
-                                               var script = '<script type="text/javascript" src="' + modules + '"></script>';
+                                               var script = mediaWiki.html.element( 'script', 
+                                                       { type: 'text/javascript', src: modules }, '' );
                                                if ( ready ) {
                                                        $( 'body' ).append( script );
                                                } else {
@@ -900,6 +904,97 @@ window.mediaWiki = new ( function( $ ) {
                $(document).ready( function() { ready = true; } );
        } )();
 
+       /** HTML construction helper functions */
+       this.html = new ( function () {
+               function escapeCallback( s ) {
+                       switch ( s ) {
+                               case "'":
+                                       return '&#039;';
+                               case '"':
+                                       return '&quot;';
+                               case '<':
+                                       return '&lt;';
+                               case '>':
+                                       return '&gt;';
+                               case '&':
+                                       return '&amp;';
+                       }
+               }
+
+               /**
+                * Escape a string for HTML. Converts special characters to HTML entities.
+                * @param s The string to escape
+                */
+               this.escape = function( s ) {
+                       return s.replace( /['"<>&]/g, escapeCallback );
+               };
+
+               /**
+                * Wrapper object for raw HTML passed to mediaWiki.html.element(). 
+                */
+               this.Raw = function( value ) {
+                       this.value = value;
+               };
+
+               /**
+                * Wrapper object for CDATA element contents passed to mediaWiki.html.element()
+                */
+               this.Cdata = function( value ) {
+                       this.value = value;
+               }
+
+               /**
+                * Create an HTML element string, with safe escaping.
+                *
+                * @param name The tag name.
+                * @param attrs An object with members mapping element names to values
+                * @param contents The contents of the element. May be either:
+                *    - string: The string is escaped.
+                *    - null or undefined: The short closing form is used, e.g. <br/>.
+                *    - this.Raw: The value attribute is included without escaping.
+                *    - this.Cdata: The value attribute is included, and an exception is 
+                *      thrown if it contains an illegal ETAGO delimiter. 
+                *      See http://www.w3.org/TR/1999/REC-html401-19991224/appendix/notes.html#h-B.3.2
+                *
+                * Example:
+                *    var h = mediaWiki.html;
+                *    return h.element( 'div', {}, 
+                *        new h.Raw( h.element( 'img', {src: '<'} ) ) );
+                * Returns <div><img src="&lt;"/></div>
+                */
+               this.element = function( name, attrs, contents ) {
+                       var s = '<' + name;
+                       for ( attrName in attrs ) {
+                               s += ' ' + attrName + '="' + this.escape( attrs[attrName] ) + '"';
+                       }
+                       if ( typeof contents == 'undefined' || contents === null ) {
+                               // Short close tag
+                               s += '/>';
+                               return s;
+                       }
+                       // Regular close tag
+                       s += '>';
+                       if (typeof contents === 'string') {
+                               // Escaped
+                               s += this.escape( contents );
+                       } else if ( contents instanceof this.Raw ) {
+                               // Raw HTML inclusion
+                               s += contents.value;
+                       } else if ( contents instanceof this.Cdata ) {
+                               // CDATA
+                               if ( /<\/[a-zA-z]/.test( contents.value ) ) {
+                                       throw new Error( 'mw.html.element: Illegal end tag found in CDATA' );
+                               }
+                               s += contents.value;
+                       } else {
+                               throw new Error( 'mw.html.element: Invalid type of contents' );
+                       }
+                       s += '</' + name + '>';
+                       return s;
+               };
+       } )();
+
+
        /* Extension points */
 
        this.legacy = {};